8W - CICD

개요

이번 주차에서 마지막으로 해볼 것은 자체적인 cicd 아키텍쳐 구축이다.
이전 문서들에서 기본적인 사전 지식을 전부 이야기했으므로, 이번에는 어떻게 아키텍처를 구축할지 구상부터 하여 바로 실습 진행에 들어간다.

참고로 원래 로컬 환경에서 구축하다가 노트북의 한계로 클러스터가 터지는 바람에 중간부터 eks에서 실습을 진행했다.
로컬 환경에서만 진행해야 했던 내용은 최대한 제외할 것이다.

실습 진행

이전에도 말했듯, CICD에 있어 가장 중요한 것은 어떻게 전략을 가져갈 것인가에 대한 것이라 생각한다.

아키텍처 구상

먼저, 내 주관을 많이 넣어서 실습을 진행하고자 한다.
많은 고민을 하면서 설계를 하되, 구축 시에는 주관을 강하게 가진 채 진행하겠다.
생각만 하고 어영부영댔던 경험이 조금 있어서, 구축을 하면서 느끼게 되는 부족함은 작업을 완료한 이후 성찰하겠다.
일주일도 안 되는 시간으로 구축해야 하기에, 시간 상 힘들 것 같은 부분들은 추가사항이라고 명시했다.

스터디 시간에 공부하면서 대략적인 구상을 마쳤다.

추가사항들을 미룬 이유

단기간에 모든 것을 하는 것은 쉽지는 않을 것으로 보여서 우선적으로 할 것들을 지정한 후 이후에 고려할 것들을 추가사항으로 남겼다.

멀티 클러스터의 경우에는 처음부터 잘 설계하지 않으면 이후에 변경이 힘든 요소가 될 수도 있는데, 이걸 보완할 수 있게 해주는 게 app of apps 패턴이라 생각하여, 완전히 못할 것도 아니라고 판단했다.

사실 과거 경험에 비춰 생각해봤을 때, 오히려 변경이 어려웠던 것은 CI 쪽에서 브랜치 전략을 어떻게 할 것이냐에 대한 것이었다.
일단 어떻게 하는 것이 효율적인가에 대한 고민도 많고, 개발 문화에 따라 너무나도 다양하게 커스텀될 수 있다보니 실제 프로젝트를 진행할 때도 이 부분이 가장 어려웠던 것 같다.
이 부분은 현재 협업을 하고 있지 않은 점, 그리고 단기간에 해야 한다는 점을 미루어 추가 사항으로 뺐다.

빌드 캐싱은 정말 순전히 시간이 부족해서이다.
그런데 이것도 대공사까지는 아니고 추가로 할 수 있겠다는 판단이 들었던 부분이 있었다.
내가 활용하려는 것은 buildkit이었는데, 문서를 보니 생각보다 정갈하게 캐싱 전략이 정리돼있었다.
일단 buildkit을 활용하려는 이유는 다음과 같다.

buildctl build ... \
  --output type=image,name=docker.io/username/image,push=true \
  --export-cache type=inline \
  --import-cache type=registry,ref=docker.io/username/image

인라인 이미지 캐싱은 이미지에 붙는 이미지 정보에 캐시를 때려박는 식.

buildctl build ... \
  --output type=image,name=localhost:5000/myrepo:image,push=true \
  --export-cache type=registry,ref=localhost:5000/myrepo:buildcache \
  --import-cache type=registry,ref=localhost:5000/myrepo:buildcache

레지스트리 캐싱
export 옵션

buildctl build ... \
  --output type=image,name=docker.io/username/image,push=true \
  --export-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image \
  --import-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image

s3로 캐싱.
이 내용들은 아주 간략하게 문서를 읽으며 정리한 내용인데, 이것도 시간만 조금 더 있으면 도전해볼 수 있을 것이라는 판단이 섰다.
로컬 캐싱만이 캐싱의 답은 아니기 때문이다.
(로컬 캐싱이면.. 어느 노드에서 빌드되게 하느냐가 엄청난 난관이 될 것 같다..)

알람 기능은 당장의 실습에 있어 코어한 부분이 아니라고 판단했다.

테라폼 백엔드의 경우, 아직 테라폼 경험이 많지 않아 조금 더 공부할 시간이 필요하다고 판단했다.

샘플 앱, 도커파일 생성

import uvicorn

from fastapi import FastAPI, Query, Header, Cookie, Body, Path, status, Request
from fastapi.responses import JSONResponse,PlainTextResponse

from typing import Annotated, Union, Optional, List, Dict, Any, TypeVar, Generic, Literal, Type, overload
from pydantic import BaseModel, Field
from enum import Enum

from pprint import pprint
import os
from datetime import datetime

from prometheus_fastapi_instrumentator import Instrumentator


app = FastAPI()
instrumentator = Instrumentator().instrument(app)
instrumentator.expose(app, include_in_schema=False)

@app.get("/", response_class=PlainTextResponse)
def read_root(request: Request, x_forwarded_for: Union[str, None] = Header(default=None, convert_underscores=True) , body = Body):
    image_tag = os.getenv("TAG", "unknown")
    now = datetime.now()
    (client_ip, client_port) = request.client
    if x_forwarded_for:
        client_ip = x_forwarded_for
    request_url = request.url

    response= "This is test FastAPI server made by Zerotay!\n"
    response+= now.strftime("The time is %-I:%M:%S %p\n")
    response+= f"TAG VERSION: {image_tag}\n"
    response+= f"Server hostname: {request_url}\n"
    response+= f"Client IP, Port: {client_ip}:{client_port}\n"
    response+= "----------------------------------\n"
    return response

if __name__ == "__main__":
    uvicorn.run(
        "main:app", 
        port=80, 
        host='0.0.0.0', 
        reload=True, 
        # ssl_keyfile= 'pki/webhook.key',
        # ssl_certfile= 'pki/webhook.crt',
    )

이전 글에서도 말 했듯이 기본적으로 내가 제작한 웹 서버는 이런 모양을 하고 있다.
단순하게 요청을 날리면 현재 시간과 빌드될 때 인자로 넘어온 태그 버전, 그리고 요청 트래픽 상 나오는 서버 주소와 클라이언트 주소를 출력한다.
여기에 프로메테우스 메트릭을 노출할 수 있도록 인스트루멘터를 추가해주었다.

# syntax=docker/dockerfile:1
# Keep this syntax directive! It's used to enable Docker BuildKit

################################
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.12-slim as python-base

    # Python
ENV PYTHONUNBUFFERED=1 \
    # pip
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PIP_DEFAULT_TIMEOUT=100 \
    \
    # Poetry
    # https://python-poetry.org/docs/configuration/#using-environment-variables
    POETRY_VERSION=2.0.1 \
    # make poetry install to this location
    POETRY_HOME="/opt/poetry" \
    # do not ask any interactive question
    POETRY_NO_INTERACTION=1 \
    # never create virtual environment automaticly, only use env prepared by us
    POETRY_VIRTUALENVS_CREATE=false \
    \
    # this is where our requirements + virtual environment will live
    VIRTUAL_ENV="/venv" 

# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VIRTUAL_ENV/bin:$PATH"

# prepare virtual env
RUN python -m venv $VIRTUAL_ENV

# working directory and Python path
WORKDIR /app
ENV PYTHONPATH="/app:$PYTHONPATH"

################################
# BUILDER-BASE
# Used to build deps + create our virtual environment
################################
FROM python-base as builder-base
RUN apt-get update && \
    apt-get install -y \
    apt-transport-https \
    gnupg \
    ca-certificates \
    build-essential \
    git \
    curl

# install poetry - respects $POETRY_VERSION & $POETRY_HOME
# The --mount will mount the buildx cache directory to where
# Poetry and Pip store their cache so that they can re-use it
RUN --mount=type=cache,target=/root/.cache \
    curl -sSL https://install.python-poetry.org | python -

# used to init dependencies
WORKDIR /app
COPY poetry.lock pyproject.toml ./
# install runtime deps to $VIRTUAL_ENV
RUN --mount=type=cache,target=/root/.cache \
    poetry install --no-root --only main

################################
# DEVELOPMENT
# Image used during development / testing
################################
FROM builder-base as development

WORKDIR /app

# quicker install as runtime deps are already installed
RUN --mount=type=cache,target=/root/.cache \
    poetry install --no-root --with test,lint

EXPOSE 80
CMD ["bash"]


################################
# PRODUCTION
# Final image used for runtime
################################
FROM python-base as production

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
    apt-get install -y --no-install-recommends \
    ca-certificates && \
    apt-get clean

# copy in our built poetry + venv
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $VIRTUAL_ENV $VIRTUAL_ENV

WORKDIR /app
COPY poetry.lock pyproject.toml ./
COPY . ./

ENV TZ=Asia/Seoul
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ARG TAG
ENV TAG=${TAG}
EXPOSE 80
CMD ["python", "./main.py"]

멀티 스테이지 빌드는 이전에도 해봐서 자신 있다고 생각했는데, 이번에 글들을 찾아보면서 내가 한 것 하꼬 수준이란 것을 다시금 깨달았다.
poetry를 사용할 때 구체적으로 어떻게 가상환경을 넣어줘야 제대로 실행이 되는지 조금 막막했는데, 이 글들을 보면서 간단하게 성공할 수 있었다.[4]
실질적으로는 그냥 거의 베껴왔고, 아주 조금의 커스텀만 더했다.[5]
기본 파이썬 베이스 이미지를 두고, 가상환경과 필요한 툴들을 배치하는 스테이지를 둔다.
이후 개발용 빌드와 실제 운영용 빌드를 구분해 빌드할 수 있도록 둔다.
image.png
간단하게 도커 허브로 테스트를 진행했는데, 일단 크기를 확실히 줄이면서 정상적으로 빌드가 됐다.
참고로 도커 허브에 올릴 땐 내 로컬에서 qemu를 활용한 빌더를 쓸 수 있어 멀티 플랫폼 빌드까지도 성공시켰다.
image.png
첫 빌드는 4분이 넘게 걸렸다.
이때 실수로 프로세스의 주소 바인딩에 아무 값도 넣지 않아서 127.0.0.1로만 트래픽을 받길래 수정해줬다.
image.png
로컬 캐싱으로 인해 두번째 빌드는 20초밖에 걸리지 않았다.
그것도 실질적으로 푸시를 하는데 걸린 시간이다.
image.png
이제 제대로 동작한다!
image.png
메트릭도 정상적으로 노출되고 있다.

환경 구축

ecr 세팅

###############################################################################
#### ECR
###############################################################################
module "ecr" {
  source = "terraform-aws-modules/ecr/aws"
  version = "2.3.1"

  repository_name = "zero-web"
  repository_type = "private"

  repository_read_write_access_arns = [
    data.aws_caller_identity.current.arn,
    # There is no node groups in eks module, so node iam role is not created in it..
    module.mng_al2023_ondemand.iam_role_arn,
    module.eks_karpenter.node_iam_role_arn
  ]
  repository_lifecycle_policy = jsonencode({
    rules = [
      {
        rulePriority = 1,
        description  = "Keep last 30 images",
        selection = {
          tagStatus     = "tagged",
          tagPrefixList = ["v"],
          countType     = "imageCountMoreThan",
          countNumber   = 30
        },
        action = {
          type = "expire"
        }
      }
    ]
  })
  repository_force_delete = true
}

resource "aws_vpc_endpoint" "s3_gateway" {
  vpc_id       = module.eks_vpc.vpc_id
  service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
  auto_accept = true

  vpc_endpoint_type = "Gateway"
  route_table_ids = [
    module.eks_vpc.pub_route_table.id,
    module.eks_vpc.priv_route_table.id
  ]
}

resource "aws_vpc_endpoint" "ecr-dkr-interface" {
  vpc_id       = module.eks_vpc.vpc_id
  service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr"
  auto_accept = true

  vpc_endpoint_type = "Interface"
  private_dns_enabled = true
  subnet_ids = concat(
    module.eks_vpc.public_subnets_id,
  )
  security_group_ids = [
    data.aws_security_group.cluster.id
  ]
}

resource "aws_vpc_endpoint" "ecr-api-interface" {
  vpc_id       = module.eks_vpc.vpc_id
  service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.api"
  auto_accept = true

  vpc_endpoint_type = "Interface"
  private_dns_enabled = true
  subnet_ids = concat(
    module.eks_vpc.public_subnets_id,
  )
  security_group_ids = [
    data.aws_security_group.cluster.id
  ]
}

data "aws_route53_zone" "my_domain" {
  name         = "zerotay.com."
}

resource "aws_route53_record" "ecr" {
  zone_id = data.aws_route53_zone.my_domain.zone_id
  name    = "hub.zerotay.com"
  type    = "CNAME"
  ttl     = 300
  records = [module.ecr.repository_url]
}

먼저 이미지 레지스트리 구축부터 진행했다.
추후 파게이트를 활용하기 위해서는 프라이빗 서브넷에서의 환경까지 고려할 필요가 있었고, 그걸 떠나서라도 외부로 트래픽이 나가지 않고 통신을 하는 것이 안전하면서도 비용 절약이 되는 관계로 vpc 엔드포인트를 뚫어주었다.
ecr 접근에 허용할 롤 설정에서 조금 헤맸는데, 내 세팅은 eks 모듈에서 노드 그룹을 생성하지 않기 때문에 eks 모듈에서 나오는 node iam 롤을 사용할 수 없었다.
어차피 카펜터로 추가된 노드들도 레포 접근이 가능해야 하기에, 내가 관리하게 되는 리소스 상에서 추출할 수 있는 모든 노드 롤들을 넣어주었다.
route53을 이용해 ecr 레포지토리 도메인 길이를 줄이고 싶었는데, 이건 실패했다.
이것도 추가사항..

(사진없음..)
vpc 내부에 들어가 nslookup으로 내부 ip가 나온다면 정상적으로 세팅된 것이다.

이제 간단하게 이미지를 올릴 수 있는지 테스트해본다.

REGISTRYNAME=여러분 ecr 도메인!
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $REGISTRYNAME
TAG=0.1.2
docker buildx build --push --build-arg TAG=$TAG --tag $REGISTRYNAME/zero-web:$TAG .

위에서도 잠시 언급했지만, docker의 buildx는 데몬으로 buildkit을 사용하고 있다.
image.png
기본적으로는 성공적으로 올라가는 것이 보인다.
image.png
이미지가 정상적으로 올라갔다.
image.png
해당 이미지를 이용해 파드를 띄워 정상 실행되는 것까지 확인할 수 있었다.

깃헙 세팅

provider "github" {
  token = trimspace(file("./github.token"))
}

resource "github_repository" "git_dev" {
  name        = "aews-dev"
  description = "for developer"
  visibility = "private"

  allow_merge_commit = true
  allow_update_branch = true
  # auto_init = true
  # gitignore_template = "Python"
}
resource "github_repository" "git_ops" {
  name        = "aews-ops"
  description = "for operator"
  visibility = "private"

  allow_merge_commit = true
  allow_update_branch = true
  # auto_init = true
}

output "git_url" {
  value = github_repository.git_dev.http_clone_url
}
output "clone" {
  value = github_repository.git_dev.git_clone_url
}

깃헙의 경우에는, 자동화가.. 가능한가? 싶었는데 테라폼에 프로바이더가 있다..![6]
깃헙도 테라폼으로 세팅할 수 있다는 것을 알게 됐으니 테스트 레포지토리를 자유롭게 만들기 편하겠다.
image.png
일단 깃헙에 접근하기 위한 토큰을 발급 받는다.
사실 어떤 권한들을 줘야 할 지 명확하게 감을 못 잡아서 거의 다 때려박았다..
image.png
인증만 제대로 된다면, 쉽게 만들어진다.
image.png
여담이지만 풀리퀘를 날리거나, 파일을 올리는 작업을 테라폼으로 할 수도 있기는 하다.

CI 세팅

처음 생각한 방향대로 구축하기 위해서는, 먼저 개발팀 깃의 변경사항에 따라 이미지를 빌드하여 ecr에 올리는 파이프라인이 필요하다.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: ci-template
spec:
  serviceAccountName: argo-workflow
  entrypoint: main
  arguments:
    parameters:
    - name: repo
      value: https://github.com/Zerotay/aews-dev.git
    - name: registry
      value: 여려분 ecr!
  templates:
    - name: main
      dag:
        tasks:
          - name: parse-tag
            template: parse-tag
            arguments:
              parameters:
              - name: ref-tag
                value: "{{workflow.parameters.ref-tag}}"
          - name: build
            template: build
            arguments:
              parameters:
              - name: registry
                value: "{{workflow.parameters.registry}}"
              - name: tag
                value: "{{tasks.parse-tag.outputs.result}}"
              artifacts:
              - name: git
                git:
                  repo: "{{workflow.parameters.repo}}"
                  revision: "main"
                  usernameSecret:
                    name: github-creds
                    key: username
                  passwordSecret:
                    name: github-creds
                    key: password
                  singleBranch: true
                  branch: main
            depends: "parse-tag"

    - name: parse-tag
      inputs:
        parameters:
        - name: ref-tag
      script:
        image: python:alpine3.6
        command: [python]
        source: |
          tag = "{{ inputs.parameters.ref-tag }}"
          tag = tag.split('/')[-1]
          print(tag)

    - name: build
      inputs:
        parameters:
        - name: registry
        - name: tag
        artifacts:
          - name: git
            path: /mnt/git
      container:
        image: moby/buildkit:v0.9.3-rootless
        workingDir: /mnt/git
        env:
          - name: BUILDKITD_FLAGS
            value: --oci-worker-no-process-sandbox
          - name: DOCKER_CONFIG
            value: /.docker
        command:
          - buildctl-daemonless.sh
        args:
          - build
          - --frontend
          - dockerfile.v0
          - --local
          - context=.
          - --local
          - dockerfile=.
          - --output
          - type=image,name={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.tag}},push=true
        readinessProbe:
          exec:
            command: [ sh, -c, "buildctl debug workers" ]
        volumeMounts:
          - name: docker-config
            mountPath: /.docker
            readOnly: true
      volumes:
        - name: docker-config
          secret:
            secretName: ecr-creds
            items:
            - key: .dockerconfigjson
              path: config.json

기본적인 워크플로우 템플릿은 이렇게 만들었다.
여러 갈래로 플로우를 나눌 수도 있겠으나, 초기 목적만 따져서보자면 굳이 그럴 필요가 없다고 판단했다.

다만 개발팀 레포에서 실제 ecr에 올릴 태그 정보를 얻기 위한 과정은 하나 추가됐다.
태그 정보는 깃 레포에서 받아온다.
그래서 깃에서 처음부터 태그를 달아야만 제대로 워크플로우가 진행된다.

도커 유저 정보는 docker-registry 유형의 시크릿을 만들 경우 .dockerconfigjson이라는 키를 가지게 되는데, buildctl의 경우 .docker/config.json 파일을 이용하도록 되어 있어 그냥 마운팅을 하면 제대로 값을 읽지 못한다.
애초에 일반 도커도 config.json을 읽는데 내가 제대로 된 활용법을 모르고 있는 것이 아닐까 싶기도 한데, 아무튼 특정 경로로 정확하게 파일을 넣기 위해 추가 설정을 넣어주었다.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowEventBinding
metadata:
  name: event-consumer
spec:
  event:
    selector: payload.sender.login == "Zerotay" && discriminator == "dev"
  submit:
    workflowTemplateRef:
      name: ci-template
    arguments:
      parameters:
      - name: ref-tag
        valueFrom:
          event: payload.ref

이 워크플로우 템플릿을 트리거할 이벤트를 바인딩하는 리소스도 필요하다.
여기에서 태그 정보도 받아오는 방식이다.
이걸 만들게 되면 다음의 경로로 워크플로우를 트리거할 수 있게 된다.

아르고서버/api/v1/events/네임스페이스/식별자(discriminator)

image.png
만들어진 바인딩 리소스가 웹으로도 확인된다.
그럼 이제 깃헙에서 웹훅 설정을 해야 한다.
image.png
깃헙 웹훅 설정에 들어와서 위의 방식으로 경로를 입력한다.
여기에서 시크릿은 아르고 워크플로우가 식별할 수 있는 임의의 문자열이기만 하면 된다.
관련한 자세한 설정은 여기 예제를 그대로 따라치면 된다.[7]
image.png
첫 술에 잘 되란 법은 없다.
image.png
처음 argo를 공부할 때 유의하라고 봤던 그 이유가 보인다.
기본적으로 아르고 시리즈들은 아르고가 설치된 네임스페이스에 대해 기본 세팅은 돼 있으나 다른 네임스페이스에 대해서는 어느 정도 커스텀을 해줘야 한다.
image.png
아주 귀찮게도 누구만 aggregate를 안 쓰는 관계로, 워크플로우 서버의 클롤 자체를 수정하는 식으로 간다.
image.png

 - apiGroups:
   - ""
   resources:
   - serviceaccounts
   verbs:
   - get
   - list

서비스어카운트는 코어 그룹이니 대애충 get list 딸깍 주자.
image.png
문제에 진전이 있다.
단순히 서비스어카운트 토큰 이름에 오타를 내서 발생한 문제였고, 다시금 서비스 어카운트 이름에 맞는 시크릿 토큰을 만들어주었다.
image.png
이렇게 200이 나오면 성공이다.
image.png

실제 빌드가 진행되는 사진 자체는 못 찍었는데..
image.png
그래도 이미지가 성공적으로 올라간 것은 확인할 수 있었다. 오예!

CD 세팅

남은 일은 이미지 레지스트리에 올라간 것을 바탕으로 cd를 하는 것인데, 사실 이건 간단하다.
내 cicd 흐름도에서는 개발팀과 운영팀이 확실하게 나뉘며 단순 버전 업데이트라 하더라도 운영팀이 한번 실제 배포 이전에 상황을 검토해야 한다.
그러므로 실제 배포는 수동으로 이뤄진다.
즉, 그냥 운영팀 레포에서 이미지 버전을 올려서 푸시를 하고, argo cd가 이 정보를 읽어 클러스터에 반영하면 성공이라는 뜻이다.
스테이징 환경에 배포하는 것은 argo image updater를 이용해서 하는 것까지 하려고 했는데, 이것도 시간이 부족해서 당장은 패스..

대신, cd를 할 운영팀은 안전하게 배포가 될 수 있도록 아르고 롤아웃을 이용하는 방식을 채택한다.
그래서 운영팀 레포의 헬름 차트에 이 사항을 구현해보자.
image.png
대충 만들어서 이랬던 놈이지만.. 이제 운영팀 레포의 헬름 차트를 수정해줘야 한다.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: canary
spec:
...
  replicas: {{ .Values.replicaCount }}
    spec:
      containers:
        - name: canary
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag}}"
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

근데 사실 시간이 부족해서 제대로 커스텀하진 못 했고, 핵심이 되는 이미지 업데이트와 레플리카 정도를 간단하게 수정할 수 있도록 Values 파일로 빼기만 했다 ㅋㅋ
헬름 차트를 만져보는 게 이번이 두번째다보니 어떻게 만져야 잘 만졌다고 소문일 날지 고민이 많이 들다보니..
오히려 그냥 냅다 하드코딩해서 당장의 시간을 단축시키는 것이 좋겠다고 판단했다.
image.png
격변이 불어오기 시작했다!
이제 이미지 업데이트를 했을 때 정상적으로 롤아웃이 진행되기만 하면 성공이다.
image.png
이미지 업데이트를 진행하자 예정대로 두 개의 레플리카셋이 보이는데, 이건 롤아웃이 제대로 작동하고 있다는 뜻이다.
image.png
사진을 제때 못 찍었는데, 롤아웃의 ui로도 업데이트가 순조롭게 완료된 것이 확인된다.
image.png
카나리 배포에서 analysis까지 성공하며 완전히 새 버전으로 배포가 전환되고 있다.
롤아웃이 완료되기 전까지, argo cd에서는 명확하게 업데이트가 진행 중인 것을 인지한다.
이거야 당연하지만, 분석이 진행되며 생기는 analysisrun 리소스까지 정확하게 추적해주어서 상태 모니터링을 하기에 정말 편리하다.
image.png
analysisrun 리소스의 소유권이 롤아웃에 걸려있기에 제대로 추적되는 것을 확인할 수 있다.
image.png
배포가 완료된 이후에는 canary 서비스 자체는 인그레스에서 트래픽을 받지 않으므로 네트워크 구조도를 볼 때 명확히 분리되는 것도 확인할 수 있다.
물론 실제 대상이 되는 파드는 stable이나 canary가 같기는 하다.

이로써, 개발팀 레포에 푸시가 일어나면 웹훅이 날아가 워크플로우가 ecr에 이미지를 빌드해서 올리는 CI 과정과,
운영팀 레포에서 이미지 버전을 업데이트하면 아르고 CD에서 이를 클러스터에 반영하고 이때 아르고 롤아웃 과정을 거쳐 점진적 배포가 되는 CD과정까지 구축이 완료됐다.

결론

뒤지게 힘들다

사실 이번에 가장 관심이 갔던 것은 빌드 캐싱 전략이었다.
시간이 부족해서 구체화하지는 못한 게 못내 아쉽다.
생각으로는 레지스트리 캐싱을 하거나, 아니면 볼륨을 하나 만들어서 캐싱 데이터를 저장한 후에 이걸 마운팅하고, 명령어 상으로는 로컬 캐싱을 하는 식으로 하는 것도 괜찮을 것 같다.
후자의 경우에는 efs와 같이 어떤 노드에서든 붙일 수 있는 스토리지를 활용하는 것이 좋을 텐데, 캐싱되는 데이터가 작지는 않을 것이라 생각이 들어서 비용적으로 좋은 선택인지 더 고민이 필요하다.

이걸 떠나서, 깃헙 액션을 이용하여 빌드를 하는 것이 더 좋은지에 대한 고민도 추가적으로 들었다.
클러스터 환경에서 빌드를 하는 것 자체가 안티 패턴이라고 보는 시각도 있던데, 깃헙 액션의 무료 횟수 제한을 넘기면 그때는 어디에서 빌드를 해야 하는가에 대한 문제에 부닥치게 되는데, 이때 클러스터 빌드를 매력적인 대안이 되지 않나 싶긴 하다.
그런데 누가 속도가 더 빠르고, 캐싱이 잘 되고에 대한 경험이 부족해서 뭐가 더 좋은 선택인지 감이 잘 오지 않는다.

추가적으로 app of apps 패턴을 통해 스테이징 클러스터와 운영 클러스터를 분리 운영을 못한 것도 아쉽다.
스테이징 클러스터의 경우 테스트를 위해 배포되는 환경이기에 이미지 버전이 업데이트되는 것만으로 자동으로 배포가 되는 환경으로서 구축해야 할 것 같다.
이 경우, 아르고 이미지 업데이터를 활용하는 것은 충분히 좋은 선택지가 될 것이라 생각한다.

이 둘은 시간나면 재도전을 해볼 것 같다.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 1 published 2025-02-03
2W - 테라폼으로 환경 구성 및 VPC 연결 2 published 2025-02-11
2W - EKS VPC CNI 분석 3 published 2025-02-11
2W - ALB Controller, External DNS 4 published 2025-02-15
3W - kubestr과 EBS CSI 드라이버 5 published 2025-02-21
3W - EFS 드라이버, 인스턴스 스토어 활용 6 published 2025-02-22
4W - 번외 AL2023 노드 초기화 커스텀 7 published 2025-02-25
4W - EKS 모니터링과 관측 가능성 8 published 2025-02-28
4W - 프로메테우스 스택을 통한 EKS 모니터링 9 published 2025-02-28
5W - HPA, KEDA를 활용한 파드 오토스케일링 10 published 2025-03-07
5W - Karpenter를 활용한 클러스터 오토스케일링 11 published 2025-03-07
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 12 published 2025-03-15
6W - api 구조와 보안 1 - 인증 13 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 14 published 2025-03-16
6W - EKS 파드에서 AWS 리소스 접근 제어 15 published 2025-03-16
6W - EKS api 서버 접근 보안 16 published 2025-03-16
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 17 published 2025-03-22
7W - EKS Fargate 18 published 2025-03-22
7W - EKS Automode 19 published 2025-03-22
8W - 아르고 워크플로우 20 published 2025-03-30
8W - 아르고 롤아웃 21 published 2025-03-30
8W - 아르고 CD 22 published 2025-03-30
8W - CICD 23 published 2025-03-30
9W - EKS 업그레이드 24 published 2025-04-02
10W - Vault를 활용한 CICD 보안 25 published 2025-04-16
11W - EKS에서 FSx, Inferentia 활용하기 26 published 2025-04-18
11주차 - EKS에서 FSx, Inferentia 활용하기 26 published 2025-05-11
12W - VPC Lattice 기반 gateway api 27 published 2025-04-27

관련 문서

이름 noteType created
Argo CD knowledge 2025-03-24
Argo Workflows knowledge 2025-03-24
Argo Rollouts knowledge 2025-03-24
아르고 롤아웃과 이스티오 연계 knowledge 2025-04-22
E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습 topic/explain 2025-03-30

참고


  1. https://medium.com/@t-velmachos/build-docker-images-on-k8s-faster-with-buildkit-3443e36aef2e ↩︎

  2. https://www.slideshare.net/slideshow/kubeconeu-building-images-efficiently-and-securely-on-kubernetes-with-buildkit/146892857 ↩︎

  3. https://docs.docker.com/build/buildkit/ ↩︎

  4. https://github.com/orgs/python-poetry/discussions/1879#discussioncomment-7284113 ↩︎

  5. https://nanmu.me/en/posts/2023/quick-dockerfile-for-python-poetry-projects/ ↩︎

  6. https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository ↩︎

  7. https://argo-workflows.readthedocs.io/en/stable/webhooks/ ↩︎